package be.mjosoft.ios.apn;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

/**
 * This class is used to access the Apple Push Notification Feedback Service.
 * @author mj
 */
public class APNFeedBackService
{
	protected static String host = "feedback.push.apple.com";
	protected static int port = 2196;
	protected static String hostDevel = "feedback.sandbox.push.apple.com";
	protected static int portDevel = 2196;

	protected boolean develMode;
	protected SSLSocketFactory sslSocketFactory;
	protected SSLSocket socket;

	/**
	 * Constructor.
	 * Switches into production mode.
	 */
	public APNFeedBackService()
	{
		this(false);
	}

	/**
	 * Constructor.
	 * @param develMode true switches into development mode, false switches into production mode.
	 */
	public APNFeedBackService(boolean develMode)
	{
		this.develMode = develMode;
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateFile the certificate file.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(String certificateFile, String certificatePWD, String certificateType) throws APNException
	{
		try{
			initConnexion(new FileInputStream(certificateFile), certificatePWD, certificateType);
		}catch(FileNotFoundException ex){
			throw new APNException(ex);
		}
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateFile the certificate file.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(File certificateFile, String certificatePWD, String certificateType) throws APNException
	{
		try{
			initConnexion(new FileInputStream(certificateFile), certificatePWD, certificateType);
		}catch(FileNotFoundException ex){
			throw new APNException(ex);
		}
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateURL the certificate url.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(URL certificateURL, String certificatePWD, String certificateType) throws APNException
	{
		try{
			URLConnection con = certificateURL.openConnection();
			initConnexion(con.getInputStream(), certificatePWD, certificateType);
		}catch(IOException ex){
			throw new APNException(ex);
		}
	}

	/**
	 * Initialises the permanent connection with the Apple Push Notification Service.
	 * @param certificateStream the certificate file stream.
	 * @param certificatePWD the certificate password.
	 * @param certificateType the certificate type.
	 * @throws APNException if an error occured.
	 */
	public void initConnexion(InputStream certificateStream, String certificatePWD, String certificateType) throws APNException
	{
		try
		{
			KeyStore keyStore = KeyStore.getInstance(certificateType);
			keyStore.load(certificateStream, certificatePWD.toCharArray());
			KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("sunx509");
			keyManagerFactory.init(keyStore, certificatePWD.toCharArray());
			TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("sunx509");
			trustManagerFactory.init(keyStore);
			SSLContext sslCtx = SSLContext.getInstance("TLS");

			TrustManager tms = new X509TrustManager()
			{
				public X509Certificate[] getAcceptedIssuers(){
					return null;
				}

				public void checkClientTrusted(X509Certificate[] certs, String authType){
				}

				public void checkServerTrusted(X509Certificate[] certs, String authType){
				}
			};
			
			sslCtx.init(keyManagerFactory.getKeyManagers(), new TrustManager[]{tms}, null);
			sslSocketFactory = sslCtx.getSocketFactory();
			connect();
		}
		catch(Exception ex)
		{
			throw new APNException(ex);
		}
	}

	/**
	 * Connects to the Appel Push Notification Service.
	 * @throws APNException if an error occured.
	 */
	protected void connect() throws APNException
	{
		try
		{
			if(develMode){
				socket = (SSLSocket)sslSocketFactory.createSocket(hostDevel, portDevel);
			}else{
				socket = (SSLSocket)sslSocketFactory.createSocket(host, port);
			}
		}
		catch(IOException ex)
		{
			throw new APNException(ex);
		}
	}

	/**
	 * Ends the connection to the Apple Push Notification Service.
	 */
	public void endConnection()
	{
		try{
			socket.close();
		}catch(Exception ex){}
	}

	/**
	 * Retreives the list of obsolete devices.
	 * That means the devices that not contains the App anymore.
	 * @return the list of obsolete devices.
	 * @throws APNException if an error occured.
	 */
  public List<ObsoleteDevice> retreiveObsoleteDevices() throws APNException
  {
		List<ObsoleteDevice> devices = new ArrayList<ObsoleteDevice>();

		byte binary[] = new byte[38];

		try
		{
			InputStream in = socket.getInputStream();
			int pos = in.read(binary, 0, 38);
			while(pos >= 38)
			{
				devices.add(process(binary));
				pos = in.read(binary, 0, 38);
			}
		}
		catch(Exception ex)
		{
			ex.printStackTrace();
		}

		return devices;
  }

	/**
	 * Parse the binary message containing information about an obsolete device.
	 * @param binary the message containing the information.
	 * @return an ObsoleteDevice object containing information about an obsolete device.
	 */
	protected ObsoleteDevice process(byte[] binary)
	{
		ObsoleteDevice device = new ObsoleteDevice();

		// Timestamp (4 bytes)
    long timestamp = binary[3];
		long tmp = (binary[2] << 8);
		timestamp += tmp & 0xFF00;
		tmp = (binary[1] << 16);
		timestamp += tmp & 0xFF0000;
		tmp = (binary[0] << 24);
		timestamp += tmp & 0xFF000000;
		timestamp *= 1000;

		Calendar date = new GregorianCalendar();
		date.setTimeInMillis(timestamp);
		device.setTimestamp(date);

		// Token length
    int tokenLength = binary[5];
		tokenLength += (binary[4] << 8) & 0xFF00;

		// Token
		StringBuilder token = new StringBuilder();
    for(int i = 0, k = 0; i < tokenLength; i++)
    {
			byte b = binary[6 + i];
			int bi = b & 0x00FF;
      String byteS = Integer.toHexString(bi);
			if(byteS.length() < 2){
				byteS = "0" + byteS;
			}
			token.append(byteS);
    }
		device.setToken(token.toString());

		return device;
	}

	/**
	 * Represents a device that is no more active for the current AppId.
	 */
	public static class ObsoleteDevice
	{
		protected String token;
		protected Calendar timestamp;

		/**
		 * Returns the token of the device.
		 * @return the token of the device.
		 */
		public String getToken()
		{
			return token;
		}

		/**
		 * Initialises the token of the device.
		 * @param token the token of the device.
		 */
		public void setToken(String token)
		{
			this.token = token;
		}

		/**
		 * Returns the timestamp that device was removed.
		 * @return the timestamp that device was removed.
		 */
		public Calendar getTimestamp()
		{
			return timestamp;
		}

		/**
		 * Initialises the timestamp that device was removed.
		 * @param timestamp the timestamp that device was removed.
		 */
		public void setTimestamp(Calendar timestamp)
		{
			this.timestamp = timestamp;
		}
	}
}